/* @flow */

import config from '../config';
import Watcher from '../observer/watcher';
import Dep, { pushTarget, popTarget } from '../observer/dep';
import { isUpdatingChildComponent } from './lifecycle';

import { set, del, observe, defineReactive, toggleObserving } from '../observer/index';

import {
    warn,
    bind,
    noop,
    hasOwn,
    hyphenate,
    isReserved,
    handleError,
    nativeWatch,
    validateProp,
    isPlainObject,
    isServerRendering,
    isReservedAttribute,
    invokeWithErrorHandling
} from '../util/index';

const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
};

export function proxy(target: Object, sourceKey: string, key: string) {
    sharedPropertyDefinition.get = function proxyGetter() {
        return this[sourceKey][key];
    };
    sharedPropertyDefinition.set = function proxySetter(val) {
        this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
}
// 初始化props/methosd/data/computed/watch
export function initState(vm: Component) {
    vm._watchers = [];
    const opts = vm.$options;
    //initProps将props转换为响应式，注入到vue实例中
    if (opts.props) initProps(vm, opts.props);
    // 初始化选项中的methods
    if (opts.methods) initMethods(vm, opts.methods);
    if (opts.data) {
        initData(vm);
    } else {
        observe((vm._data = {}), true /* asRootData */);
    }
    if (opts.computed) initComputed(vm, opts.computed);
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch);
    }
}

function initProps(vm: Component, propsOptions: Object) {
    const propsData = vm.$options.propsData || {};
    const props = (vm._props = {});
    // cache prop keys so that future props updates can iterate using Array
    // instead of dynamic object key enumeration.
    // 缓存 prop ，以便以后的 props 更新可以使用Array进行迭代
    // 而不是动态对象键枚举
    const keys = (vm.$options._propKeys = []);
    const isRoot = !vm.$parent;
    // root instance props should be converted
    // props 根实例应该被转换
    if (!isRoot) {
        toggleObserving(false);
    }
    for (const key in propsOptions) {
        keys.push(key);
        const value = validateProp(key, propsOptions, propsData, vm);
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
            const hyphenatedKey = hyphenate(key);
            if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) {
                warn(
                    `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
                    vm
                );
            }
            defineReactive(props, key, value, () => {
                if (!isRoot && !isUpdatingChildComponent) {
                    warn(
                        `Avoid mutating a prop directly since the value will be ` +
                            `overwritten whenever the parent component re-renders. ` +
                            `Instead, use a data or computed property based on the prop's ` +
                            `value. Prop being mutated: "${key}"`,
                        vm
                    );
                }
            });
        } else {
            defineReactive(props, key, value);
        }
        // static props are already proxied on the component's prototype
        // during Vue.extend(). We only need to proxy props defined at
        // instantiation here.
        if (!(key in vm)) {
            proxy(vm, `_props`, key);
        }
    }
    toggleObserving(true);
}

function initData(vm: Component) {
    let data = vm.$options.data;
    //初始化 _data，如果组件中data是函数，调用函数返回结果
    //否则直接返回data
    data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
    // isPlainObject 是不是原生的object
    if (!isPlainObject(data)) {
        data = {};
        // 生成模式下，vue data必须返回一个对象
        process.env.NODE_ENV !== 'production' &&
            warn(
                'data functions should return an object:\n' +
                    'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
                vm
            );
    }
    // proxy data on instance
    // 获取 data 中的所有属性
    const keys = Object.keys(data);
    // 获取 props 和 methods
    const props = vm.$options.props;
    const methods = vm.$options.methods;
    let i = keys.length;
    // data中属性值不能和 props 中属性值同名
    while (i--) {
        const key = keys[i];
        // hasOwn 对象自身属性中是否具有指定的属性
        if (process.env.NODE_ENV !== 'production') {
            if (methods && hasOwn(methods, key)) {
                warn(`Method "${key}" has already been defined as a data property.`, vm);
            }
        }
        if (props && hasOwn(props, key)) {
            process.env.NODE_ENV !== 'production' &&
                warn(
                    `The data property "${key}" is already declared as a prop. ` +
                        `Use prop default value instead.`,
                    vm
                );
        }
        // isReserved 检查属性名是否以 $ 或 _ 开头
        else if (!isReserved(key)) {
            proxy(vm, `_data`, key);
        }
    }
    // observe data
    // 数据响应式处理。参数2：asRootData.是否为根数据？
    observe(data, true /* asRootData */);
}

export function getData(data: Function, vm: Component): any {
    // #7573 disable dep collection when invoking data getters
    pushTarget();
    try {
        return data.call(vm, vm);
    } catch (e) {
        handleError(e, vm, `data()`);
        return {};
    } finally {
        popTarget();
    }
}

const computedWatcherOptions = { lazy: true };

function initComputed(vm: Component, computed: Object) {
    // $flow-disable-line
    const watchers = (vm._computedWatchers = Object.create(null));
    // computed properties are just getters during SSR
    const isSSR = isServerRendering();

    for (const key in computed) {
        const userDef = computed[key];
        const getter = typeof userDef === 'function' ? userDef : userDef.get;
        if (process.env.NODE_ENV !== 'production' && getter == null) {
            warn(`Getter is missing for computed property "${key}".`, vm);
        }

        if (!isSSR) {
            // create internal watcher for the computed property.
            watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
        }

        // component-defined computed properties are already defined on the
        // component prototype. We only need to define computed properties defined
        // at instantiation here.
        if (!(key in vm)) {
            defineComputed(vm, key, userDef);
        } else if (process.env.NODE_ENV !== 'production') {
            if (key in vm.$data) {
                warn(`The computed property "${key}" is already defined in data.`, vm);
            } else if (vm.$options.props && key in vm.$options.props) {
                warn(`The computed property "${key}" is already defined as a prop.`, vm);
            } else if (vm.$options.methods && key in vm.$options.methods) {
                warn(`The computed property "${key}" is already defined as a method.`, vm);
            }
        }
    }
}

export function defineComputed(target: any, key: string, userDef: Object | Function) {
    const shouldCache = !isServerRendering();
    if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache
            ? createComputedGetter(key)
            : createGetterInvoker(userDef);
        sharedPropertyDefinition.set = noop;
    } else {
        sharedPropertyDefinition.get = userDef.get
            ? shouldCache && userDef.cache !== false
                ? createComputedGetter(key)
                : createGetterInvoker(userDef.get)
            : noop;
        sharedPropertyDefinition.set = userDef.set || noop;
    }
    if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function () {
            warn(`Computed property "${key}" was assigned to but it has no setter.`, this);
        };
    }
    Object.defineProperty(target, key, sharedPropertyDefinition);
}

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }
            if (Dep.target) {
                watcher.depend();
            }
            return watcher.value;
        }
    };
}

function createGetterInvoker(fn) {
    return function computedGetter() {
        return fn.call(this, this);
    };
}

function initMethods(vm: Component, methods: Object) {
    const props = vm.$options.props;
    for (const key in methods) {
        if (process.env.NODE_ENV !== 'production') {
            if (typeof methods[key] !== 'function') {
                warn(
                    `Method "${key}" has type "${typeof methods[
                        key
                    ]}" in the component definition. ` +
                        `Did you reference the function correctly?`,
                    vm
                );
            }
            if (props && hasOwn(props, key)) {
                warn(`Method "${key}" has already been defined as a prop.`, vm);
            }
            if (key in vm && isReserved(key)) {
                warn(
                    `Method "${key}" conflicts with an existing Vue instance method. ` +
                        `Avoid defining component methods that start with _ or $.`
                );
            }
        }
        vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
}

function initWatch(vm: Component, watch: Object) {
    for (const key in watch) {
        const handler = watch[key];
        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i]);
            }
        } else {
            createWatcher(vm, key, handler);
        }
    }
}

function createWatcher(vm: Component, expOrFn: string | Function, handler: any, options?: Object) {
    if (isPlainObject(handler)) {
        options = handler;
        handler = handler.handler;
    }
    if (typeof handler === 'string') {
        handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options);
}

export function stateMixin(Vue: Class<Component>) {
    // flow somehow has problems with directly declared definition object
    // when using Object.defineProperty, so we have to procedurally build up
    // the object here.
    const dataDef = {};
    dataDef.get = function () {
        return this._data;
    };
    const propsDef = {};
    propsDef.get = function () {
        return this._props;
    };
    if (process.env.NODE_ENV !== 'production') {
        dataDef.set = function () {
            warn(
                'Avoid replacing instance root $data. ' + 'Use nested data properties instead.',
                this
            );
        };
        // 不允许直接给 $data、$set 直接赋值
        propsDef.set = function () {
            warn(`$props is readonly.`, this);
        };
    }
    Object.defineProperty(Vue.prototype, '$data', dataDef);
    Object.defineProperty(Vue.prototype, '$props', propsDef);

    //将set和del方法挂载到vue原型上
    Vue.prototype.$set = set;
    Vue.prototype.$delete = del;
    // 监听数据变化
    Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
    ): Function {
        // 获取Vue实例this
        const vm: Component = this;
        if (isPlainObject(cb)) {
            // 判断如果cb是对象执行createWatcher
            return createWatcher(vm, expOrFn, cb, options);
        }
        options = options || {};
        // 标记为用户watcher
        options.user = true;
        // 创建用户watcher对象
        const watcher = new Watcher(vm, expOrFn, cb, options);
        // 判断immediate，如果为true
        if (options.immediate) {
            const info = `callback for immediate watcher "${watcher.expression}"`;
            //立即执行
            pushTarget();
            invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
            popTarget();
        }
        // 取消监听器
        return function unwatchFn() {
            watcher.teardown();
        };
    };
}
